package com.taglab.chic.pagedata;

import java.io.IOException;
import java.io.InputStream;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import javax.xml.stream.events.XMLEvent;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.io.Resource;
import org.springframework.util.Assert;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

/**
 * @author Markus Kobler
 * @author Peter O'Shaughnessy
 */
public class MetaDataInterceptor extends HandlerInterceptorAdapter implements InitializingBean {

  private final Logger logger = LoggerFactory.getLogger( this.getClass() );
  
  private Resource pageDefinitions;

  private Map<String,PageData> pages;
  
  /**
   * Number of seconds to wait in between checking the last modified time of the page definitions resource.
   * A value of 0 or a negative value will mean it checks every time, but a higher value is recommended.
   */
  private int definitionsCheckDelaySeconds = 0;
  
  private long definitionsLastModified;
  private Date nextDefinitionsCheckAfter;
  private Lock definitionsLock = new ReentrantLock();

  public void setPageDefinitions(Resource pageDefinitions) {
    this.pageDefinitions = pageDefinitions;
  }
  
  public void setDefinitionsCheckDelaySeconds(int definitionsCheckDelaySeconds) {
    this.definitionsCheckDelaySeconds = definitionsCheckDelaySeconds;
  }
  
  public void afterPropertiesSet() throws Exception {
    Assert.notNull(pageDefinitions, "Page definitions cannot be null");
    Assert.isTrue(pageDefinitions.exists(), pageDefinitions+" does not exist");
    doDefinitionsCheck();
  }

  @Override
  public void postHandle(HttpServletRequest request,
                         HttpServletResponse response,
                         Object handler,
                         ModelAndView modelAndView) throws Exception {

    if( isTimeToCheckDefinitions() ) doDefinitionsCheck();    
    
    PageData pageData = pages.get(request.getRequestURI());

    if( pageData != null ) {
      @SuppressWarnings("unchecked")      
      Map model = modelAndView.getModel();
      addValueIfNull(model, "pageTitle", pageData.getTitle());
      addValueIfNull(model, "pageDescription", pageData.getDescription());
      addValueIfNull(model, "pageKeywords", pageData.getKeywords());
    }

  }

  @SuppressWarnings("unchecked")
  protected void addValueIfNull(Map model, String key, String value) {
    if( value == null ) return;
    String oldValue = (String)model.get(key);
    if( oldValue == null ) {
      model.put(key, value);
    }
  }
  
  protected void doDefinitionsCheck() {
    
    if( definitionsChanged() ) parsePages();
    
    if( definitionsCheckDelaySeconds > 0 ) {
      nextDefinitionsCheckAfter = new Date( System.currentTimeMillis() + (definitionsCheckDelaySeconds * 1000)); 
      logger.debug( "Next definitions check after {}", nextDefinitionsCheckAfter );
    }
    
  }
  
  private void parsePages() {
    
    PageParser parser = new PageParser();
   
    /**
     * Only one thread should parse the definitions at a time. If other threads attempt to do this at 
     * the same time then they should fall through immediately and simply use the previous set of definitions.
     */
    if( definitionsLock.tryLock() ) { 
      
      try {
        
        logger.debug( "Parsing page definitions" );
        
        this.pages = parser.parsePages(pageDefinitions);

        try {
          definitionsLastModified = pageDefinitions.lastModified();
        } catch( IOException ioe ) {
          logger.warn("Could not read page definitions last modified time", ioe);
        }
        
      } finally {
        definitionsLock.unlock();
      }
      
    }
    
  }

  private boolean isTimeToCheckDefinitions() {
    return nextDefinitionsCheckAfter == null || new Date().after( nextDefinitionsCheckAfter );
  }
  
  private boolean definitionsChanged() {
    
    try {
      
      boolean changed = definitionsLastModified < pageDefinitions.lastModified();
      
      if( changed ) logger.debug( "Definitions have changed" );
    
      return changed;
      
    } catch( IOException ioe ) {
      logger.warn("Could not read page definitions last modified time", ioe);
      return false;
    }
    
  }
  
  /**
   * Useful for testing.
   */
  protected void expireDefinitions() {
    nextDefinitionsCheckAfter = new Date( System.currentTimeMillis() - 1 );
  }
  

  private static class PageParser {

    private static XMLInputFactory inputFactory = XMLInputFactory.newInstance();

    public Map<String,PageData> parsePages(Resource file) {

      Map<String,PageData> pages = new HashMap<String,PageData>();

      InputStream is = null;
      XMLStreamReader reader = null;
      PageData currentPage = null;

      try {

        is = file.getInputStream();
        reader = inputFactory.createXMLStreamReader(is);

        String curElement;

        while( reader.hasNext() ) {

          switch (reader.next()) {

            case XMLEvent.START_ELEMENT:
              curElement = reader.getLocalName();
              if( curElement.equals("page")) {
                currentPage = new PageData(reader.getAttributeValue("", "path"));
                pages.put(currentPage.getPath(), currentPage);
              } else if( curElement.equals("title")) {
                if( reader.next() == XMLEvent.CHARACTERS ) {
                  currentPage.setTitle(reader.getText());
                }
              } else if( curElement.equals("descripton")) {
                currentPage.setDescription(reader.getAttributeValue("", "content"));
              } else if( curElement.equals("keywords")) {
                currentPage.setKeywords(reader.getAttributeValue("", "content"));
              }
              break;
          }
        }

      } catch( Exception ex ) {
        throw new RuntimeException(ex);
      } finally {

        try {
          if( reader != null ) reader.close();
        } catch( XMLStreamException ex) { /* ignore */ }

        try {
          if( is != null ) is.close();
        } catch( IOException ex) { /* ignore */ }

      }

      return pages;
    }

  }

}
